package br.senai.sc.controller.crud;

import java.io.Serializable;
import java.math.BigDecimal;
import java.util.List;
import java.util.Set;

import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.PersistenceContext;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;

import org.jboss.seam.international.status.Messages;
import org.jboss.seam.international.status.builder.BundleKey;

import br.senai.sc.controller.business.CorpoHidricoBusiness;
import br.senai.sc.controller.business.PontoColetaBusiness;
import br.senai.sc.controller.exception.BusinessException;
import br.senai.sc.model.Amostra;
import br.senai.sc.model.Cidade;
import br.senai.sc.model.PontoColeta;
import br.senai.sc.util.SystemConstants;

/**
 * Responsável pelas funções que envolvem banco de dados da entidade
 * {@link PontoColeta}.
 * 
 * @author Tiago Wanke Marques &lt;tiago.marques@sc.senai.br&gt;
 */
public class PontoColetaCRUD implements Serializable {

	private static final long serialVersionUID = 1L;

	@PersistenceContext
	private EntityManager em;

	@Inject
	private Messages messages;

	@Inject
	private Validator validator;

	@Inject
	private PontoColetaBusiness pontoColetaBusiness;

	@Inject
	private CorpoHidricoBusiness corpoHidricoBusiness;

	/**
	 * @param idPontoColeta
	 * @return {@link PontoColeta} a partir do id informado.
	 */
	public PontoColeta findPontoColeta(Integer idPontoColeta) {

		if (idPontoColeta == null) {
			throw new IllegalArgumentException(
					"Passado identificador null para procurar ponto de coleta.");
		}

		return em.find(PontoColeta.class, idPontoColeta);
	}

	/**
	 * Busca por todos os pontos de coleta cadastrados no sistema.
	 * 
	 * @return Retorna os pontos de coleta encontrados.
	 */
	public List<PontoColeta> findPontosColeta() {
		return em
				.createNamedQuery(PontoColeta.PONTOS_COLETA, PontoColeta.class)
				.getResultList();
	}

	/**
	 * @param cidade
	 * @return Lista de {@link PontoColeta} de uma {@link Cidade}.
	 */
	public List<PontoColeta> findPontosColeta(Cidade cidade) {

		if ((cidade == null) || (cidade.getIdCidade() == null)) {
			throw new IllegalArgumentException(
					"Passado cidade null para procurar pontos por cidade.");
		}

		return em
				.createNamedQuery(PontoColeta.PONTOS_COLETA_BY_CIDADE,
						PontoColeta.class).setParameter("cidade", cidade)
				.getResultList();
	}

	/**
	 * Retorna as quatro últimas {@link Amostra} do {@link PontoColeta}
	 * informado.
	 * 
	 * @param idPontoColeta
	 * @return Lista de {@link Amostra} a partir do {@link PontoColeta}
	 *         informado.
	 */
	public List<Amostra> findUltimasAmostras(Integer idPontoColeta) {

		if (idPontoColeta == null) {
			throw new IllegalArgumentException(
					"Passado identificador null para procurar amostras por ponto de coleta.");
		}

		final int qtdAmostras = 4;
		return em.createNamedQuery(Amostra.AMOSTRAS_BY_PONTO, Amostra.class)
				.setParameter("idPontoColeta", idPontoColeta)
				.setMaxResults(qtdAmostras).getResultList();
	}

	/**
	 * Busca a última amostra cadastrada de um ponto de coleta.
	 * 
	 * @param idPontoColeta
	 *            ID do ponto de coleta que será buscada a amostra.
	 * @return Retorna a amostrada encontrada ou nulo.
	 */
	public Amostra findUltimaAmostra(Integer idPontoColeta) {

		if (idPontoColeta == null) {
			throw new IllegalArgumentException(
					"Passado identificador null para procurar ponto de coleta.");
		}

		try {
			return em
					.createNamedQuery(PontoColeta.ULTIMAS_AMOSTRAS,
							Amostra.class)
					.setParameter("idPontoColeta", idPontoColeta)
					.setMaxResults(1).getSingleResult();
		} catch (NoResultException e) {
			return null;
		}
	}

	/**
	 * Calcula a coordenada central levando em consideração todos os
	 * {@link PontoColeta} cadastrados. Para isto, pega a maior e a menor
	 * longitude e latitude e efetua o seguinte cálculo:
	 * 
	 * -1 * (Math.abs(menorLatitude) + ((Math.abs(maiorLatitude) -
	 * Math.abs(menorLatitude)) / 2)), -1 * (Math.abs(menorLongitude) +
	 * ((Math.abs(maiorLongitude) - Math.abs(menorLongitude)) / 2)),
	 * 
	 * @return Coordenada central dos {@link PontoColeta} cadastrados. Caso não
	 *         houver {@link PontoColeta} cadastrado retorna null.
	 */
	public float[] findCoordenadaCentral() {
		Object[] coordenadas = (Object[]) em
				.createNamedQuery(
						PontoColeta.MAIOR_E_MENOR_LATITUDE_E_LONGITUDE)
				.setMaxResults(1).getSingleResult();

		if ((coordenadas[0] == null) || (coordenadas[1] == null)
				|| (coordenadas[2] == null) || (coordenadas[3] == null)) {
			return null;
		}

		float maiorLatitude = ((BigDecimal) coordenadas[0]).floatValue();
		float menorLatitude = ((BigDecimal) coordenadas[1]).floatValue();
		float maiorLongitude = ((BigDecimal) coordenadas[2]).floatValue();
		float menorLongitude = ((BigDecimal) coordenadas[3]).floatValue();

		return new float[] {
				-1
						* (Math.abs(menorLatitude) + ((Math.abs(maiorLatitude) - Math
								.abs(menorLatitude)) / 2)),
				-1
						* (Math.abs(menorLongitude) + ((Math
								.abs(maiorLongitude) - Math.abs(menorLongitude)) / 2)), };
	}

	/**
	 * @return Última {@link Amostra} de todos os pontos cadastrados no sistema.
	 *         Caso não encontre nenhum ponto retorna uma lista vazia.
	 */
	@SuppressWarnings("unchecked")
	public List<Amostra> findUltimasAmostras() {

		return em.createNamedQuery(Amostra.ULTIMAS_AMOSTRAS_BY_PONTO)
				.getResultList();
	}

	/**
	 * Converte a lista de {@link ConstraintViolation} para uma lista de
	 * {@link BundleKey}.
	 * 
	 * @param constraints
	 */
	private void toBundleKeyList(
			Set<ConstraintViolation<PontoColeta>> constraints) {

		for (ConstraintViolation<PontoColeta> constraint : constraints) {
			messages.error(new BundleKey(
					SystemConstants.PATH_TO_COMMON_MESSAGES, constraint
							.getMessageTemplate()));
		}

	}

	/**
	 * Persiste os dados de um {@link PontoColeta}.
	 * 
	 * @param pontoColeta
	 * @return {@link PontoColeta} persistido.
	 * @throws BusinessException
	 *             Lançada caso houver algum problema de validação.
	 */
	public PontoColeta salvar(PontoColeta pontoColeta) throws BusinessException {

		if (pontoColeta == null) {
			throw new IllegalArgumentException(
					"Tentando salvar ponto coleta informando null.");
		}

		Set<ConstraintViolation<PontoColeta>> constraints = validator
				.validate(pontoColeta);
		if (constraints.size() > 0) {
			toBundleKeyList(constraints);
			throw new BusinessException();
		}

		if (pontoColeta.getIdPontoColeta() == null) {
			inserir(pontoColeta);
			return pontoColeta;
		}

		return atualizar(pontoColeta);
	}

	/**
	 * Caso não houver nenhum erro de validação, persiste a entidade.
	 * 
	 * @param pontoColeta
	 * @throws BusinessException
	 *             Lançada caso houver algum erro de validação.
	 */
	private void inserir(PontoColeta pontoColeta) throws BusinessException {

		if (!pontoColetaBusiness.podeSalvar(pontoColeta)) {
			throw new BusinessException();
		}

		em.persist(pontoColeta);
	}

	/**
	 * Efetua as regras de negócio para atualiza {@link PontoColeta}.
	 * 
	 * @param pontoColeta
	 * @return {@link PontoColeta} atualizado.
	 * @throws BusinessException
	 *             Lançada caso houver algum erro de validação.
	 */
	private PontoColeta atualizar(PontoColeta pontoColeta)
			throws BusinessException {

		if (!pontoColetaBusiness.podeEditar(pontoColeta)) {
			throw new BusinessException();
		}

		return em.merge(pontoColeta);
	}

	public void flush() {
		em.flush();
	}

	/**
	 * Remove um {@link PontoColeta} do banco de dados. Este método efetua um
	 * merge na entidade antes de remove-la para garantir que a mesma esteja
	 * attached.
	 * 
	 * @param pontoColeta
	 * @throws BusinessException
	 *             Lançada caso houver alguma validação que não seja atendida.
	 */
	public void remover(PontoColeta pontoColeta) throws BusinessException {

		// a verificação de null não é feita aqui, pois o metodo
		// corpoHidricoBusiness.existe já garante esta verificação
		if (!pontoColetaBusiness.podeRemover(pontoColeta)) {
			throw new BusinessException();
		}

		em.remove(em.merge(pontoColeta)); // garante que a entidade seja
											// attached
	}
}
